function createElement(tag, attrs, ...children) {
    return {
        tag,
        attrs,
        children
    }
}

// 这里 vnode 是 element 对象
function render(vnode, container) {
    container.appendChild(_render(vnode))
}

// 在这里将 element 转换成真实的 dom
function _render(vnode) {
    if (vnode === undefined || vnode === null || typeof vnode === 'boolean') {
        vnode = '';
    }
    if (typeof vnode === 'undefined') {
        vnode = String(vnode)
    }
    if (typeof vnode.tag === 'function') {
        // 组件
        let component = createComponent(vnode.tag, vnode.attrs)
        setComponentProps(component, vnode.attrs)
        return component.base
    } else if (typeof vnode === 'string') {
        return document.createTextNode(vnode)
    } else {
        let dom = document.createElement(vnode.tag)
        if (vnode.attrs) {
            Object.keys(vnode.attrs).forEach(k => {
                let value = vnode.attrs[k]
                setAttribute(dom, k, value)
            })
        }
        // 其实这里有一个递归
        vnode.children.forEach(child => render(child, dom))
        return dom
    }
}

function createComponent(component, props) {
    let inst
    if (component.prototype && component.prototype.render) {
        inst = new component(props)
    } else {
        // 函数组件
        inst = new component(props)
        inst.constructor = component
        inst.render = function () {
            return this.constructor(props)
        }
    }
    return inst
}

// 更新 props
// 触发回调
function setComponentProps(component, props) {
    if (!component.base) {
        if (component.componentWillMount) {
            component.componentWillMount()
        } else if (component.componentWillReceiveProps) {
            component.componentWillReceiveProps(props)
        }
    }
    component.props = props
    renderComponent(component)
}

// 每次 setState 都调用,
// 但是目前，每次都需要全部重新刷新
function renderComponent(component) {
    // 这里 base 名字，不是特别的理解 ???
    let base
    // render 返回的是一个 jsx 也就是 element 对象
    // 因为 jsx 会被编译成 createElement() 的调用
    let renderer = component.render()
    if (component.base && component.componentWillUpdate) {
        component.componentWillUpdate()
    }
    // render 的作用就是把 element 转化成 dom (或者其他宿主环境的元素)
    // 如果是组件，则会继续调用，直到普通的 dom 元素
    base = _render(renderer)
    if (component.base) {
        if (component.componentDidUpdate) {
            component.componentDidUpdate()
        } else if (component.componentDidMount) {
            component.componentDidUpdate()
        }
    }
    if (component.base && component.base.parentNode) {
        component.base.parentNode.replaceChild(base, component.base)
    }
    component.base = base
    base._component = component
}

function diff(dom, vnode, container) {
    let ret = diffNode(dom, vnode)
    if (container && ret.parentNode !== container) {
        container.appendChild(ret)
    }
    return ret
}

// Vdom 和 dom 之间的 diff
function diffNode(dom, vnode) {
    let out = dom
    if (vnode === undefined || vnode === null || typeof vnode === 'boolean') {
        vnode = ''
    }
    if (typeof vnode === 'number') {
        vnode = String(vnode);
    }

    if (typeof vnode === 'string') {
        // 文本节点
        if (dom && dom.nodeType === 3) {
            if (dom.textContent !== vnode) {
                dom.textContent = vnode
            } else {
                // same text
            }
        } else {
            out = document.createTextNode(vnode)
            if (dom && dom.parentNode) {
                dom.parentNode.replaceChild(out, dom)
            }
        }
        return out
    } else if (typeof vnode.tag === 'function') {
        // component
        return diffComponent(dom, vnode)
    } else {
        // dom
        if (!dom || dom.nodeName.toLowerCase() !== vnode.tag.toLowerCase()) {
            out = document.createElement(vnode.tag)
            if (dom) {
                [...dom.childNodes].map(out.appendChild)
                if (dom.parentNode) {
                    dom.parentNode.replaceChild(out, dom)
                }
            }
        }
    }
    if (vnode.children && vnode.children.length > 0 || (out.childNodes && out.childNodes.length > 0)) {
        diffChildren(out, vnode.children)
    }
    diffChildren(out, vnode.children)
    return out
}

function diffAttributes(dom, vnode) {
    let old = {}
    let attrs = vnode.attrs

    for (let i = 0; i < dom.attributes.length; i++) {
        let attr = dom.attributes[i]
        old[attr.name] = attr.value
    }

    for (let name in old) {
        if (!(name in attrs)) {
            setAttribute(dom, name, undefined)
        }
    }

    for (let name in attrs) {
        if (old[name] !== attrs[name]) {
            setAttribute(dom, name, attrs[name])
        }
    }
}

function diffChildren(dom, vchildren) {
    let domChildren = dom.childNodes
    let children = []
    let keyed = {}

    if (domChildren.length > 0) {
        for (let i = 0; i < domChildren.length; i++) {
            let child = domChildren[i]
            let key = child.key
            if (key) {
                keyed[key] = child
            } else {
                children.push(child)
            }
        }
    }

    if (vchildren && vchildren.length > 0) {
        let min = 0
        let childrenLen = children.length
        for (let i = 0; i < vchildren.length; i++) {
            let vchild = vchildren[i]
            let key = vchild.key
            let child

            if (key) {
                if (keyed[key]) {
                    child = keyed[key]
                    keyed[key] = undefined
                }
            } else if (min < childrenLen) {
                for (let j = min; j < childrenLen; j++) {
                    let c = children[j]
                    if (c && isSameNodeType(c, vchild)) {
                        child = c
                        children[j] = undefined
                        if (j === childrenLen - 1) {
                            childrenLen -= 1
                        }
                        if (j === min) {
                            min++
                        }
                        break
                    }
                }
            }
        }
    }

    child = diff(child, vchild)

    let f = domChildren[i]
    if (child && child !== dom && child !== f) {
        if (!f) {
            dom.appendChild(child)
        } else if (child === f.nextSibling) {
            removeNode(f)
        } else {
            dom.insertBefore(child, f)
        }
    }
}

function diffComponent(dom, component) {
    let c = dom && dom._component
    let oldDom = dom
    if (c && c.constructor === vnode.tag) {
        setComponentProps(c, vnode.attrs)
        dom = c.base
    } else {
        if (c) {
            unmountComponent(c)
            oldDom = null
        }
        c = createComponent(vnode.tag, vnode.attrs)
        setComponentProps(c, vnode.attrs)
        dom = c.base
        if (oldDom && dom !== oldDom) {
            oldDom._component = null
            removeNode(oldDom)
        }
    }
    return dom
}

function setAttribute(dom, name, value) {
    if (name === 'className') {
        name = 'class'
    } else if (/on\w+/.test(name)) {
        name = name.toLowerCase()
        dom[name] = value || ''
    } else if (name === 'style') {
        if (!value || typeof value === 'string') {
            dom.style.cssText = value || ''
        } else if (value && typeof value === 'object') {
            for (let name in value) {
                let v
                if (typeof value[name] === 'number') {
                    v = `${value[name]}px`
                } else {
                    v = value[name]
                }
                dom.style[name] = v
            }
        } else {
            for (name in dom) {
                dom[name] = value || ''
            }
            if (value) {
                dom.setAttribute(name, value)
            } else {
                dom.removeAttribute(name)
            }
        }
    }
}

class Component {
    constructor(props) {
        this.state = {}
        this.props = props
    }

    setState(stateChange) {
        Object.assign(this.state, stateChange)
        renderComponent(this)
    }
}

const EReact = {
    Component,
    createElement,
    render: function (vnode, container) {
        container.innerHTML = ''
        return render(vnode, container)
    }
}

// app

class Counter extends EReact.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0,
        }
    }

    componentWillUpdate() {
        console.dir('update')
    }

    componentWillMount() {
        console.dir('mount')
    }

    onClick() {
        this.setState({
            count: this.state.count + 1,
        })
    }

    // 组件里面的 render 和 EReact 上的那个功能不一样
    render() {
        let inner = EReact.createElement(
            'div',
            {
                onClick: () => this.onClick()
            },
            `count: ${this.state.count}`
        )
        return EReact.createElement('div', {
            style: {
                color: 'red',
            }
        }, inner)
    }
}

EReact.render(
    createElement(Counter),
    document.getElementById('app'),
)
